package com.liuqianfei.markdown;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;


public class Processor
{
    /** The reader. */
    private final Reader reader;
    /** The emitter. */
    private final Emitter emitter;
    /** The Configuration. */
    final Configuration config;
    /** Extension flag. */
    private boolean useExtensions = false;

    /**
     * Constructor.
     *
     * @param reader The input reader.
     */
    private Processor(final Reader reader, final Configuration config)
    {
        this.reader = reader;
        this.config = config;
        this.useExtensions = config.forceExtendedProfile;
        this.emitter = new Emitter(this.config);
    }

    /**
     * Transforms an input stream into HTML using the given Configuration.
     *
     * @param reader The Reader to process.
     * @param configuration The Configuration.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration
     */
    public final static String process(final Reader reader, final Configuration configuration) throws IOException
    {
        final Processor p = new Processor(!(reader instanceof BufferedReader) ? new BufferedReader(reader) : reader, configuration);
        return p.process();
    }

    /**
     * Transforms an input String into HTML using the given Configuration.
     *
     * @param input The String to process.
     * @param configuration The Configuration.
     * @return The processed String.
     * @see Configuration
     */
    public final static String process(final String input, final Configuration configuration)
    {
        try
        {
            return process(new StringReader(input), configuration);
        }
        catch (final IOException e)
        {
            // This _can never_ happen
            return null;
        }
    }

    /**
     * Transforms an input file into HTML using the given Configuration.
     *
     * @param file The File to process.
     * @param configuration the Configuration
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration
     */
    public final static String process(final File file, final Configuration configuration) throws IOException
    {
        final FileInputStream input = new FileInputStream(file);
        final String ret = process(input, configuration);
        input.close();
        return ret;
    }

    /**
     * Transforms an input stream into HTML using the given Configuration.
     *
     * @param input The InputStream to process.
     * @param configuration The Configuration.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration
     */
    public final static String process(final InputStream input, final Configuration configuration) throws IOException
    {
        final Processor p = new Processor(new BufferedReader(new InputStreamReader(input, configuration.encoding)), configuration);
        return p.process();
    }

    /**
     * Transforms an input String into HTML using the default Configuration.
     *
     * @param input The String to process.
     * @return The processed String.
     * @see Configuration#DEFAULT
     */
    public final static String process(final String input)
    {
        return process(input, Configuration.DEFAULT);
    }

    /**
     * Transforms an input String into HTML.
     *
     * @param input The String to process.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @see Configuration#DEFAULT
     */
    public final static String process(final String input, final boolean safeMode)
    {
        return process(input, Configuration.builder().setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input String into HTML.
     *
     * @param input The String to process.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @see Configuration#DEFAULT
     */
    public final static String process(final String input, final Decorator decorator)
    {
        return process(input, Configuration.builder().setDecorator(decorator).build());
    }

    /**
     * Transforms an input String into HTML.
     *
     * @param input The String to process.
     * @param decorator The decorator to use.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @see Configuration#DEFAULT
     */
    public final static String process(final String input, final Decorator decorator, final boolean safeMode)
    {
        return process(input, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input file into HTML using the default Configuration.
     *
     * @param file The File to process.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file) throws IOException
    {
        return process(file, Configuration.DEFAULT);
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file The File to process.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final boolean safeMode) throws IOException
    {
        return process(file, Configuration.builder().setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file The File to process.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final Decorator decorator) throws IOException
    {
        return process(file, Configuration.builder().setDecorator(decorator).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file The File to process.
     * @param decorator The decorator to use.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final Decorator decorator, final boolean safeMode) throws IOException
    {
        return process(file, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file The File to process.
     * @param encoding The encoding to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final String encoding) throws IOException
    {
        return process(file, Configuration.builder().setEncoding(encoding).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file The File to process.
     * @param encoding The encoding to use.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final String encoding, final boolean safeMode) throws IOException
    {
        return process(file, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file The File to process.
     * @param encoding The encoding to use.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final String encoding, final Decorator decorator) throws IOException
    {
        return process(file, Configuration.builder().setEncoding(encoding).setDecorator(decorator).build());
    }

    /**
     * Transforms an input file into HTML.
     *
     * @param file The File to process.
     * @param encoding The encoding to use.
     * @param decorator The decorator to use.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final File file, final String encoding, final Decorator decorator, final boolean safeMode) throws IOException
    {
        return process(file, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode).setDecorator(decorator).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input The InputStream to process.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input) throws IOException
    {
        return process(input, Configuration.DEFAULT);
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input The InputStream to process.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final boolean safeMode) throws IOException
    {
        return process(input, Configuration.builder().setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input The InputStream to process.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final Decorator decorator) throws IOException
    {
        return process(input, Configuration.builder().setDecorator(decorator).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input The InputStream to process.
     * @param decorator The decorator to use.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final Decorator decorator, final boolean safeMode) throws IOException
    {
        return process(input, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input The InputStream to process.
     * @param encoding The encoding to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final String encoding) throws IOException
    {
        return process(input, Configuration.builder().setEncoding(encoding).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input The InputStream to process.
     * @param encoding The encoding to use.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final String encoding, final boolean safeMode) throws IOException
    {
        return process(input, Configuration.builder().setEncoding(encoding).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input The InputStream to process.
     * @param encoding The encoding to use.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final String encoding, final Decorator decorator) throws IOException
    {
        return process(input, Configuration.builder().setEncoding(encoding).setDecorator(decorator).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param input The InputStream to process.
     * @param encoding The encoding to use.
     * @param decorator The decorator to use.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final InputStream input, final String encoding, final Decorator decorator, final boolean safeMode) throws IOException
    {
        return process(input, Configuration.builder().setEncoding(encoding).setDecorator(decorator).setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input stream into HTML using the default Configuration.
     *
     * @param reader The Reader to process.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final Reader reader) throws IOException
    {
        return process(reader, Configuration.DEFAULT);
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param reader The Reader to process.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final Reader reader, final boolean safeMode) throws IOException
    {
        return process(reader, Configuration.builder().setSafeMode(safeMode).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param reader The Reader to process.
     * @param decorator The decorator to use.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final Reader reader, final Decorator decorator) throws IOException
    {
        return process(reader, Configuration.builder().setDecorator(decorator).build());
    }

    /**
     * Transforms an input stream into HTML.
     *
     * @param reader The Reader to process.
     * @param decorator The decorator to use.
     * @param safeMode Set to <code>true</code> to escape unsafe HTML tags.
     * @return The processed String.
     * @throws IOException if an IO error occurs
     * @see Configuration#DEFAULT
     */
    public final static String process(final Reader reader, final Decorator decorator, final boolean safeMode) throws IOException
    {
        return process(reader, Configuration.builder().setDecorator(decorator).setSafeMode(safeMode).build());
    }

    /**
     * Reads all lines from our reader.
     * <p>
     * Takes care of markdown link references.
     * </p>
     *
     * @return A Block containing all lines.
     * @throws IOException If an IO error occurred.
     */
    private Block readLines() throws IOException
    {
        final Block block = new Block();
        final StringBuilder sb = new StringBuilder(80);
        int c = this.reader.read();
        LinkReference lastLinkReference = null;
        while (c != -1)
        {
            sb.setLength(0);
            int pos = 0;
            boolean eol = false;
            while (!eol)
            {
                switch (c)
                {
                    case -1:
                        eol = true;
                        break;
                    case '\n':
                        c = this.reader.read();
                        if (c == '\r')
                        {
                            c = this.reader.read();
                        }
                        eol = true;
                        break;
                    case '\r':
                        c = this.reader.read();
                        if (c == '\n')
                        {
                            c = this.reader.read();
                        }
                        eol = true;
                        break;
                    case '\t':
                    {
                        final int np = pos + (4 - (pos & 3));
                        while (pos < np)
                        {
                            sb.append(' ');
                            pos++;
                        }
                        c = this.reader.read();
                        break;
                    }
                    default:
                        if (c != '<' || !this.config.panicMode)
                        {
                            pos++;
                            sb.append((char) c);
                        }
                        else
                        {
                            pos += 4;
                            sb.append("&lt;");
                        }
                        c = this.reader.read();
                        break;
                }
            }

            final Line line = new Line();
            line.value = sb.toString();
            line.init();

            // Check for link definitions
            boolean isLinkReference = false;
            String id = null, link = null, comment = null;
            if (!line.isEmpty && line.leading < 4 && line.value.charAt(line.leading) == '[')
            {
                line.position = line.leading + 1;
                // Read ID up to ']'
                id = line.readUntil(']');
                // Is ID valid and are there any more characters?
                if (id != null && line.position + 2 < line.value.length())
                {
                    // Check for ':' ([...]:...)
                    if (line.value.charAt(line.position + 1) == ':')
                    {
                        line.position += 2;
                        line.skipSpaces();
                        // Check for link syntax
                        if (line.value.charAt(line.position) == '<')
                        {
                            line.position++;
                            link = line.readUntil('>');
                            line.position++;
                        }
                        else
                        {
                            link = line.readUntil(' ', '\n');
                        }

                        // Is link valid?
                        if (link != null)
                        {
                            // Any non-whitespace characters following?
                            if (line.skipSpaces())
                            {
                                final char ch = line.value.charAt(line.position);
                                // Read comment
                                if (ch == '\"' || ch == '\'' || ch == '(')
                                {
                                    line.position++;
                                    comment = line.readUntil(ch == '(' ? ')' : ch);
                                    // Valid linkRef only if comment is valid
                                    if (comment != null)
                                    {
                                        isLinkReference = true;
                                    }
                                }
                            }
                            else
                            {
                                isLinkReference = true;
                            }
                        }
                    }
                }
            }

            // To make compiler happy: add != null checks
            if (isLinkReference && id != null && link != null)
            {
                if (id.toLowerCase().equals("$profile$"))
                {
                    this.emitter.useExtensions = this.useExtensions = link.toLowerCase().equals("extended");
                    lastLinkReference = null;
                }
                else
                {
                    // Store linkRef and skip line
                    final LinkReference lr = new LinkReference(link, comment, comment != null && (link.length() == 1 && link.charAt(0) == '*'));
                    this.emitter.addLinkReference(id, lr);
                    if (comment == null)
                    {
                        lastLinkReference = lr;
                    }
                }
            }
            else
            {
                comment = null;
                // Check for multi-line linkRef
                if (!line.isEmpty && lastLinkReference != null)
                {
                    line.position = line.leading;
                    final char ch = line.value.charAt(line.position);
                    if (ch == '\"' || ch == '\'' || ch == '(')
                    {
                        line.position++;
                        comment = line.readUntil(ch == '(' ? ')' : ch);
                    }
                    if (comment != null)
                    {
                        lastLinkReference.title = comment;
                    }

                    lastLinkReference = null;
                }

                // No multi-line linkRef, store line
                if (comment == null)
                {
                    line.position = 0;
                    block.appendLine(line);
                }
            }
        }

        return block;
    }

    /**
     * Initializes a list block by separating it into list item blocks.
     *
     * @param root The Block to process.
     */
    private void initListBlock(final Block root)
    {
        Line line = root.lines;
        line = line.next;
        while (line != null)
        {
            final LineType t = line.getLineType(this.config);
            if ((t == LineType.OL || t == LineType.UL) || (!line.isEmpty && (line.prevEmpty && line.leading == 0 && !(t == LineType.OL || t == LineType.UL))))
            {
                root.split(line.prev).type = BlockType.LIST_ITEM;
            }
            line = line.next;
        }
        root.split(root.lineTail).type = BlockType.LIST_ITEM;
    }

    /**
     * Recursively process the given Block.
     *
     * @param root The Block to process.
     * @param listMode Flag indicating that we're in a list item block.
     */
    private void recurse(final Block root, final boolean listMode)
    {
        Block block, list;
        Line line = root.lines;

        if (listMode)
        {
            root.removeListIndent(this.config);
            if (this.useExtensions && root.lines != null && root.lines.getLineType(this.config) != LineType.CODE)
            {
                root.id = root.lines.stripID();
            }
        }

        while (line != null && line.isEmpty)
        {
            line = line.next;
        }
        if (line == null)
        {
            return;
        }

        while (line != null)
        {
            final LineType type = line.getLineType(this.config);
            switch (type)
            {
                case OTHER:
                {
                    final boolean wasEmpty = line.prevEmpty;
                    while (line != null && !line.isEmpty)
                    {
                        final LineType t = line.getLineType(this.config);
                        if ((listMode || this.useExtensions) && (t == LineType.OL || t == LineType.UL))
                        {
                            break;
                        }
                        if (this.useExtensions && (t == LineType.CODE || t == LineType.FENCED_CODE))
                        {
                            break;
                        }
                        if (t == LineType.HEADLINE || t == LineType.HEADLINE1 || t == LineType.HEADLINE2 || t == LineType.HR || t == LineType.BLOCKQUOTE || t == LineType.XML)
                        {
                            break;
                        }
                        line = line.next;
                    }
                    final BlockType bt;
                    if (line != null && !line.isEmpty)
                    {
                        bt = (listMode && !wasEmpty) ? BlockType.NONE : BlockType.PARAGRAPH;
                        root.split(line.prev).type = bt;
                        root.removeLeadingEmptyLines();
                    }
                    else
                    {
                        bt = (listMode && (line == null || !line.isEmpty) && !wasEmpty) ? BlockType.NONE : BlockType.PARAGRAPH;
                        root.split(line == null ? root.lineTail : line).type = bt;
                        root.removeLeadingEmptyLines();
                    }
                    line = root.lines;
                    break;
                }
                case CODE:
                    while (line != null && (line.isEmpty || line.leading > 3))
                    {
                        line = line.next;
                    }
                    block = root.split(line != null ? line.prev : root.lineTail);
                    block.type = BlockType.CODE;
                    block.removeSurroundingEmptyLines();
                    break;
                case XML:
                    if (line.prev != null)
                    {
                        // FIXME ... this looks wrong
                        root.split(line.prev);
                    }
                    root.split(line.xmlEndLine).type = BlockType.XML;
                    root.removeLeadingEmptyLines();
                    line = root.lines;
                    break;
                case BLOCKQUOTE:
                    while (line != null)
                    {
                        if (!line.isEmpty && (line.prevEmpty && line.leading == 0 && line.getLineType(this.config) != LineType.BLOCKQUOTE))
                        {
                            break;
                        }
                        line = line.next;
                    }
                    block = root.split(line != null ? line.prev : root.lineTail);
                    block.type = BlockType.BLOCKQUOTE;
                    block.removeSurroundingEmptyLines();
                    block.removeBlockQuotePrefix();
                    this.recurse(block, false);
                    line = root.lines;
                    break;
                case HR:
                    if (line.prev != null)
                    {
                        // FIXME ... this looks wrong
                        root.split(line.prev);
                    }
                    root.split(line).type = BlockType.RULER;
                    root.removeLeadingEmptyLines();
                    line = root.lines;
                    break;
                case FENCED_CODE:
                    line = line.next;
                    while (line != null)
                    {
                        if (line.getLineType(this.config) == LineType.FENCED_CODE)
                        {
                            break;
                        }
                        // TODO ... is this really necessary? Maybe add a special flag?
                        line = line.next;
                    }
                    if (line != null)
                    {
                        line = line.next;
                    }
                    block = root.split(line != null ? line.prev : root.lineTail);
                    
                    // 修正，相邻两个多行代码块，如果中间是空行，将导致后面代码库识别错误的问题。
                    if (block.lines.isEmpty)
                    {
                        block.removeLeadingEmptyLines();
                    }
                    
                    block.type = BlockType.FENCED_CODE;
                    block.meta = Utils.getMetaFromFence(block.lines.value);
                    block.lines.setEmpty();
                    if (block.lineTail.getLineType(this.config) == LineType.FENCED_CODE)
                    {
                        block.lineTail.setEmpty();
                    }
                    block.removeSurroundingEmptyLines();
                    break;
                case HEADLINE:
                case HEADLINE1:
                case HEADLINE2:
                    if (line.prev != null)
                    {
                        root.split(line.prev);
                    }
                    if (type != LineType.HEADLINE)
                    {
                        line.next.setEmpty();
                    }
                    block = root.split(line);
                    block.type = BlockType.HEADLINE;
                    if (type != LineType.HEADLINE)
                    {
                        block.hlDepth = type == LineType.HEADLINE1 ? 1 : 2;
                    }
                    if (this.useExtensions)
                    {
                        block.id = block.lines.stripID();
                    }
                    block.transfromHeadline();
                    root.removeLeadingEmptyLines();
                    line = root.lines;
                    break;
                case OL:
                case UL:
                    while (line != null)
                    {
                        final LineType t = line.getLineType(this.config);
                        if (!line.isEmpty && (line.prevEmpty && line.leading == 0 && !(t == LineType.OL || t == LineType.UL)))
                        {
                            break;
                        }
                        line = line.next;
                    }
                    list = root.split(line != null ? line.prev : root.lineTail);
                    list.type = type == LineType.OL ? BlockType.ORDERED_LIST : BlockType.UNORDERED_LIST;
                    list.lines.prevEmpty = false;
                    list.lineTail.nextEmpty = false;
                    list.removeSurroundingEmptyLines();
                    list.lines.prevEmpty = list.lineTail.nextEmpty = false;
                    this.initListBlock(list);
                    block = list.blocks;
                    while (block != null)
                    {
                        this.recurse(block, true);
                        block = block.next;
                    }
                    list.expandListParagraphs();
                    break;
                default:
                    line = line.next;
                    break;
            }
        }
    }

    /**
     * Does all the processing.
     *
     * @return The processed String.
     * @throws IOException If an IO error occurred.
     */
    private String process() throws IOException
    {
        final StringBuilder out = new StringBuilder();
        final Block parent = this.readLines();
        parent.removeSurroundingEmptyLines();

        this.recurse(parent, false);
        Block block = parent.blocks;
        while (block != null)
        {
            this.emitter.emit(out, block);
            block = block.next;
        }

        return out.toString();
    }
}
